package com.ansion.commonchat.startup.workmanager.bwork.utils;

import static com.ansion.commonchat.startup.workmanager.bwork.BWorkInfo.State.BLOCKED;
import static com.ansion.commonchat.startup.workmanager.bwork.BWorkInfo.State.CANCELLED;
import static com.ansion.commonchat.startup.workmanager.bwork.BWorkInfo.State.ENQUEUED;
import static com.ansion.commonchat.startup.workmanager.bwork.BWorkInfo.State.FAILED;
import static com.ansion.commonchat.startup.workmanager.bwork.BWorkInfo.State.RUNNING;
import static com.ansion.commonchat.startup.workmanager.bwork.BWorkInfo.State.SUCCEEDED;

import android.annotation.SuppressLint;
import android.content.Context;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.ansion.commonchat.startup.workmanager.bwork.BData;
import com.ansion.commonchat.startup.workmanager.bwork.BListenableWorker;
import com.ansion.commonchat.startup.workmanager.bwork.BWorkInfo;
import com.ansion.commonchat.startup.workmanager.bwork.BWorkerFactory;
import com.ansion.commonchat.startup.workmanager.bwork.BWorkerParameters;
import com.ansion.commonchat.startup.workmanager.bwork.impl.BScheduler;
import com.ansion.commonchat.startup.workmanager.bwork.impl.BSchedulers;
import com.ansion.commonchat.startup.workmanager.bwork.impl.mode.BDependencyDao;
import com.ansion.commonchat.startup.workmanager.bwork.impl.mode.BWorkSpec;
import com.ansion.commonchat.startup.workmanager.bwork.impl.mode.BWorkSpecDao;
import com.ansion.commonchat.startup.workmanager.bwork.impl.mode.BWorkTagDao;
import com.ansion.commonchat.startup.workmanager.bwork.impl.mode.WorkDataCache;
import com.ansion.commonchat.startup.workmanager.bwork.utils.futures.BSettableFuture;
import com.ansion.commonchat.startup.workmanager.bwork.utils.taskexecutor.BTaskExecutor;
import com.ansion.log.LogTag;
import com.ansion.log.VLog;
import com.google.common.util.concurrent.ListenableFuture;

import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;

/**
 * Created by wanghongjie on 2023/5/11
 * Describe : 用于包装和管理单个 Worker 的执行
 * <p>
 * 1.管理 Worker 的执行和生命周期。
 * 它负责创建和管理Work线程、调用工作的 startWork() 方法以执行实际的工作逻辑，并处理工作的状态转换和错误处理。
 */
public class BWorkerWrapper implements Runnable {

    Context mAppContext;
    private String mWorkSpecId;
    private List<BScheduler> mSchedulers;
    BWorkSpec mWorkSpec;
    BListenableWorker mWorker;
    Executor mExecutor;
    BTaskExecutor mWorkTaskExecutor;
    BWorkerFactory mWorkerFactory;
    @NonNull
    BListenableWorker.Result mResult = BListenableWorker.Result.failure();

    private WorkDataCache mWorkDataCache;
    private BWorkSpecDao mWorkSpecDao;
    private BDependencyDao mDependencyDao;
    private BWorkTagDao mWorkTagDao;

    private List<String> mTags;
    private String mWorkDescription;
    @NonNull
    BSettableFuture<Boolean> mFuture = BSettableFuture.create();
    @Nullable
    ListenableFuture<BListenableWorker.Result> mInnerFuture = null;

    private volatile boolean mInterrupted;

    BWorkerWrapper(@NonNull Builder builder) {
        mAppContext = builder.mAppContext;
        mExecutor = builder.mExecutor;
        mWorkTaskExecutor = builder.mWorkTaskExecutor;
        mWorkerFactory = builder.mWorkerFactory;
        mWorkSpecId = builder.mWorkSpecId;
        mSchedulers = builder.mSchedulers;
        mWorker = builder.mWorker;

        mWorkDataCache = builder.mWorkDataCache;
        mWorkSpecDao = mWorkDataCache.workSpecDao();
        mDependencyDao = mWorkDataCache.dependencyDao();
        mWorkTagDao = mWorkDataCache.workTagDao();
    }

    public @NonNull
    ListenableFuture<Boolean> getFuture() {
        return mFuture;
    }

    @Override
    public void run() {
        mTags = mWorkTagDao.getTagsForWorkSpecId(mWorkSpecId);
        mWorkDescription = createWorkDescription(mTags);
        runWorker();
    }

    private void runWorker() {
        if (tryCheckForInterruptionAndResolve()) {
            return;
        }

        mWorkSpec = mWorkSpecDao.getWorkSpec(mWorkSpecId);
        if (mWorkSpec == null) {
            if (VLog.OPEN_LOG)
                VLog.e(LogTag.TAG_B_WORK_MANAGER, "Didn't find WorkSpec for id %s", mWorkSpecId);
            resolve(false);
            return;
        }

        if (mWorkSpec.state != ENQUEUED) {
            resolveIncorrectStatus();
            if (VLog.OPEN_LOG)
                VLog.d(LogTag.TAG_B_WORK_MANAGER, "%s is not in ENQUEUED state. Nothing more to do.",
                        mWorkSpec.workerClassName);
            return;
        }

        final BWorkerParameters params = new BWorkerParameters(
                UUID.fromString(mWorkSpecId),
                mWorkSpec.input,
                mTags,
                mExecutor,
                mWorkTaskExecutor);

        if (mWorker == null) {
            mWorker = mWorkerFactory.createWorkerWithDefaultFallback(
                    mAppContext,
                    mWorkSpec.workerClassName,
                    params);
        }

        if (mWorker == null) {
            if (VLog.OPEN_LOG)
                VLog.e(LogTag.TAG_B_WORK_MANAGER, "Could not create BWorker %s", mWorkSpec.workerClassName);
            setFailedAndResolve();
            return;
        }

        if (mWorker.isUsed()) {
            if (VLog.OPEN_LOG)
                VLog.e(LogTag.TAG_B_WORK_MANAGER, "Received an already-used BWorker %s; BWorkerFactory should return "
                                + "new instances",
                        mWorkSpec.workerClassName);
            setFailedAndResolve();
            return;
        }
        mWorker.setUsed();

        if (trySetRunning()) {
            if (tryCheckForInterruptionAndResolve()) {
                return;
            }

            final BSettableFuture<BListenableWorker.Result> future = BSettableFuture.create();
            // 启动 Work，监听 future ，去除 前台相关逻辑
            mInnerFuture = mWorker.startWork();
            future.setFuture(mInnerFuture);
            final String workDescription = mWorkDescription;
            future.addListener(new Runnable() {
                @Override
                @SuppressLint("SyntheticAccessor")
                public void run() {
                    try {
                        BListenableWorker.Result result = future.get();
                        if (result == null) {
                            if (VLog.OPEN_LOG)
                                VLog.e(LogTag.TAG_B_WORK_MANAGER, "%s returned a null result. Treating it as a failure.",
                                        mWorkSpec.workerClassName);
                        } else {
                            if (VLog.OPEN_LOG)
                                VLog.e(LogTag.TAG_B_WORK_MANAGER, "%s returned a %s result.",
                                        mWorkSpec.workerClassName, result);
                            mResult = result;
                        }
                    } catch (CancellationException exception) {
                        if (VLog.OPEN_LOG)
                            VLog.e(LogTag.TAG_B_WORK_MANAGER, "%s was cancelled , >> %s ", workDescription, exception.getMessage());
                    } catch (InterruptedException | ExecutionException exception) {
                        if (VLog.OPEN_LOG)
                            VLog.e(LogTag.TAG_B_WORK_MANAGER, "%s failed because it threw an exception/error ,  >> %s ", workDescription, exception.getMessage());
                    } finally {
                        onWorkFinished();
                    }
                }
            }, mWorkTaskExecutor.getBackgroundExecutor());
        } else {
            resolveIncorrectStatus();
        }
    }

    void onWorkFinished() {
        if (!tryCheckForInterruptionAndResolve()) {
            BWorkInfo.State state = mWorkSpecDao.getState(mWorkSpecId);
            /**
             * 1.如果状态为null，这种情况可能出现在使用beginUniqueWork()方法时使用了REPLACE策略。将其视为失败，并调用resolve(false)方法来解决。这将变为无操作，但仍然需要通知可能持有唤醒锁的潜在观察者。
             * 2.如果状态为RUNNING，表示工作任务正在运行中。调用handleResult(mResult)方法来处理工作任务的结果。
             * 3.如果状态未完成（非FINISHED状态），表示工作任务需要重新调度。调用rescheduleAndResolve()方法来重新调度并解决工作任务。
             */
            if (state == null) {
                resolve(false);
            } else if (state == RUNNING) {
                handleResult(mResult);
            } else if (!state.isFinished()) {
                rescheduleAndResolve();
            }
        }

        /**
         * 1.首先，取消其他调度器中的该工作任务。例如，如果该工作任务由GreedyScheduler处理，那么需要确保通知JobScheduler取消该作业，并且AlarmManager取消所有相关的闹钟。
         * 2.使用scheduler.cancel(mWorkSpecId)取消在其他调度器中的工作任务。
         * 3.调用Schedulers.schedule(mConfiguration, mWorkDatabase, mSchedulers)来重新调度工作任务。这将基于给定的配置、工作数据库和调度器列表进行重新调度。
         */
        if (mSchedulers != null) {
            for (BScheduler scheduler : mSchedulers) {
                scheduler.cancel(mWorkSpecId);
            }
            // 调度下一批任务
            BSchedulers.schedule(mWorkDataCache, mSchedulers);
        }
    }

    public void interrupt() {
        mInterrupted = true;
        tryCheckForInterruptionAndResolve();
        boolean isDone = false;
        if (mInnerFuture != null) {
            isDone = mInnerFuture.isDone();
            mInnerFuture.cancel(true);
        }
        if (mWorker != null && !isDone) {
            mWorker.stop();
        } else {
            if (VLog.OPEN_LOG)
                VLog.d(LogTag.TAG_B_WORK_MANAGER, "WorkSpec %s is already done. Not interrupting.", mWorkSpec);
        }
    }

    private void resolveIncorrectStatus() {
        BWorkInfo.State status = mWorkSpecDao.getState(mWorkSpecId);
        if (status == RUNNING) {
            if (VLog.OPEN_LOG)
                VLog.e(LogTag.TAG_B_WORK_MANAGER, "Status for %s is RUNNING;"
                        + "not doing any work and rescheduling for later execution", mWorkSpecId);
            resolve(true);
        } else {
            if (VLog.OPEN_LOG)
                VLog.e(LogTag.TAG_B_WORK_MANAGER, "Status for %s is %s; not doing any work", mWorkSpecId, status);
            resolve(false);
        }
    }

    private boolean tryCheckForInterruptionAndResolve() {
        if (mInterrupted) {
            if (VLog.OPEN_LOG)
                VLog.d(LogTag.TAG_B_WORK_MANAGER, String.format("Work interrupted for %s", mWorkDescription));
            BWorkInfo.State currentState = mWorkSpecDao.getState(mWorkSpecId);
            if (currentState == null) {
                resolve(false);
            } else {
                resolve(!currentState.isFinished());
            }
            return true;
        }
        return false;
    }

    private void resolve(final boolean needsReschedule) {
        if (needsReschedule) {
            mWorkSpecDao.setState(ENQUEUED, mWorkSpecId);
        }
        mFuture.set(needsReschedule);
    }

    private void handleResult(BListenableWorker.Result result) {
        if (result instanceof BListenableWorker.Result.Success) {
            if (VLog.OPEN_LOG)
                VLog.d(LogTag.TAG_B_WORK_MANAGER, "BWorker result SUCCESS for %s", mWorkDescription);
            setSucceededAndResolve();
        } else {
            if (VLog.OPEN_LOG)
                VLog.d(LogTag.TAG_B_WORK_MANAGER, "BWorker result FAILURE for %s", mWorkDescription);
            setFailedAndResolve();
        }
    }

    private boolean trySetRunning() {
        boolean setToRunning = false;
        BWorkInfo.State currentState = mWorkSpecDao.getState(mWorkSpecId);
        if (currentState == ENQUEUED) {
            mWorkSpecDao.setState(RUNNING, mWorkSpecId);
            setToRunning = true;
        }
        return setToRunning;
    }

    private void iterativelyFailWorkAndDependents(String workSpecId) {
        @SuppressWarnings("JdkObsolete")
        LinkedList<String> idsToProcess = new LinkedList<>();
        idsToProcess.add(workSpecId);
        while (!idsToProcess.isEmpty()) {
            String id = idsToProcess.remove();
            if (mWorkSpecDao.getState(id) != CANCELLED) {
                mWorkSpecDao.setState(FAILED, id);
            }
            List<String> dependentWorkIds = mDependencyDao.getDependentWorkIds(id);
            if (dependentWorkIds != null) {
                idsToProcess.addAll(mDependencyDao.getDependentWorkIds(id));
            }
        }
    }

    void setFailedAndResolve() {
        iterativelyFailWorkAndDependents(mWorkSpecId);
        BListenableWorker.Result.Failure failure = (BListenableWorker.Result.Failure) mResult;
        BData output = failure.getOutputData();
        mWorkSpecDao.setOutput(mWorkSpecId, output);
        resolve(false);
    }

    private void rescheduleAndResolve() {
        mWorkSpecDao.setState(ENQUEUED, mWorkSpecId);
        resolve(true);
    }

    private void setSucceededAndResolve() {
        mWorkSpecDao.setState(SUCCEEDED, mWorkSpecId);
        BListenableWorker.Result.Success success = (BListenableWorker.Result.Success) mResult;
        BData output = success.getOutputData();
        mWorkSpecDao.setOutput(mWorkSpecId, output);

        List<String> dependentWorkIds = mDependencyDao.getDependentWorkIds(mWorkSpecId);
        if (dependentWorkIds != null) {
            for (String dependentWorkId : dependentWorkIds) {
                if (mWorkSpecDao.getState(dependentWorkId) == BLOCKED
                        && mDependencyDao.hasCompletedAllPrerequisites(dependentWorkId)) {
                    if (VLog.OPEN_LOG)
                        VLog.d(LogTag.TAG_B_WORK_MANAGER, "Setting status to enqueued for %s", dependentWorkId);
                    mWorkSpecDao.setState(ENQUEUED, dependentWorkId);
                }
            }
        }
        resolve(false);
    }

    private String createWorkDescription(List<String> tags) {
        StringBuilder sb = new StringBuilder("Work [ id=")
                .append(mWorkSpecId)
                .append(", tags={ ");

        boolean first = true;
        for (String tag : tags) {
            if (first) {
                first = false;
            } else {
                sb.append(", ");
            }
            sb.append(tag);
        }
        sb.append(" } ]");

        return sb.toString();
    }

    public static class Builder {

        @NonNull
        Context mAppContext;
        BListenableWorker mWorker;
        @NonNull
        Executor mExecutor;
        @NonNull
        BTaskExecutor mWorkTaskExecutor;
        @NonNull
        BWorkerFactory mWorkerFactory;
        @NonNull
        WorkDataCache mWorkDataCache;
        @NonNull
        String mWorkSpecId;
        List<BScheduler> mSchedulers;

        public Builder(@NonNull Context context,
                       @NonNull Executor executor,
                       @NonNull BTaskExecutor workTaskExecutor,
                       @NonNull BWorkerFactory workerFactory,
                       @NonNull WorkDataCache workDataCache,
                       @NonNull String workSpecId) {
            mAppContext = context.getApplicationContext();
            mExecutor = executor;
            mWorkTaskExecutor = workTaskExecutor;
            mWorkerFactory = workerFactory;
            mWorkDataCache = workDataCache;
            mWorkSpecId = workSpecId;
        }

        @NonNull
        public Builder withSchedulers(@NonNull List<BScheduler> schedulers) {
            mSchedulers = schedulers;
            return this;
        }

        @NonNull
        public BWorkerWrapper build() {
            return new BWorkerWrapper(this);
        }
    }
}